In [2]:
import numpy as np

Exercise 07.1 (indexing and timing)

Create two very long NumPy arrays x and y and sum the arrays using:

  1. The NumPy addition syntax, z = x + y; and
  2. A for loop that computes the sum entry-by-entry

Compare the time required for the two approaches for vectors of different lengths. The values of the array entries are not important for this test.

Hint: To loop over an array using indices, try a construction like:

x = np.ones(100)
y = np.ones(len(x))
for i in range(len(x)):
    print(x[i]*y[i])

Timing NumPy addition for 1 million elements arrays


In [2]:
n = 1000000
x = np.random.rand(n)
y = np.random.rand(n)

%time z = x + y


Wall time: 3.01 ms

Timing 1 million elements arrays addition using an entry-by-entry function


In [3]:
def sum_vec(x, y):
    "Sum two vectors entry by entry"
    z = np.zeros(n)
    for i in range(n):
        z[i] = x[i] + y[i]
    return z

%time w = sum_vec(x, y)


Wall time: 476 ms

Exercise 07.2 (member functions and slicing)

Anonymised scores (out of 60) for an examination are stored in a NumPy array. Write:

  1. A function that takes a NumPy array of the raw scores and returns the scores as a percentage sorted from lowest to highest (try using scores.sort(), where scores is a NumPy array holding the scores).
  2. A function that returns the maximum, minimum and mean of the raw scores as a dictionary with the keys 'min', 'max' and 'mean'. Use the NumPy array functions min(), max() and mean() to do the computation, e.g. max = scores.max().
  3. Modify your function for the min, max and mean to optionally exclude the highest and lowest scores from the computation of the min, max and mean. Hint: sort the array of scores and use array slicing to exclude the first and the last entries.

Use the scores

scores = np.array([58.0, 35.0, 24.0, 42, 7.8])

In [4]:
# Test scores
scores = np.array([58.0, 35.0, 24.0, 42, 7.8])

Function that takes a NumPy array of the raw scores and returns the scores as a percentage sorted from lowest to highest


In [5]:
def percentages(scores):
    "Calculate percentages (max score = 60) from a list of scores and returns them sorted"
    sorted_scores = scores / 60
    sorted_scores.sort()
    return sorted_scores

print(percentages(scores))


[ 0.13        0.4         0.58333333  0.7         0.96666667]

Function that returns the maximum, minimum and mean of the raw scores as a dictionary


In [6]:
def max_min_mean(scores):
    "Return a dictionary with max, min and mean score from a list of scores"
    out = {}
    out['min'] = scores.min()
    out['max'] = scores.max()
    out['mean'] = scores.mean()
    return out

print(max_min_mean(scores))


{'min': 7.7999999999999998, 'max': 58.0, 'mean': 33.359999999999999}

Modify your function for the min, max and mean to optionally exclude the highest and lowest scores


In [7]:
def max_min_mean2(scores, exclude_extreme):
    "Return a dictionary with max, min and mean score from a list of scores excluding extremes if exclude_extreme = 1"
    out = {}
    # If extremes are excluded filter the sorted list from second to second to last element
    if exclude_extreme == 1:
        scores.sort()
        scores_filtered = scores[1:-1]
    # Else use the entire list
    elif exclude_extreme == 0:
        scores_filtered = scores
    # If exclude_extreme is not 0 nor 1 return a message
    else:
        return 'The second parameter should be either 0 (to include extremes) or 1 (to exclude them)'
    
    out['min'] = scores_filtered.min()
    out['max'] = scores_filtered.max()
    out['mean'] = scores_filtered.mean()  
    return out

print(max_min_mean2(scores, 1))


{'min': 24.0, 'max': 42.0, 'mean': 33.666666666666664}

Exercise 07.3 (slicing)

For the two-dimensional array


In [8]:
A = np.array([[4.0, 7.0, -2.43, 67.1],
             [-4.0, 64.0, 54.7, -3.33],
             [2.43, 23.2, 3.64, 4.11],
             [1.2, 2.5, -113.2, 323.22]])
print(A)


[[   4.      7.     -2.43   67.1 ]
 [  -4.     64.     54.7    -3.33]
 [   2.43   23.2     3.64    4.11]
 [   1.2     2.5  -113.2   323.22]]

use array slicing to

  1. Extract the third column as a 1D array
  2. Extract the first two rows as a 2D sub-array
  3. Extract the bottom-right $2 \times 2$ block as a 2D sub-array
  4. Sum the last column

Print the results to the screen to check. Try to use array slicing such that your code would still work if the dimensions of A were enlarged.

Also, compute the tranpose of A (search online to find the function/syntax to do this).

Third column as a 1D array


In [9]:
print(A[:,2])


[  -2.43   54.7     3.64 -113.2 ]

First two rows as a 2D sub-array


In [10]:
print(A[:2,:])


[[  4.     7.    -2.43  67.1 ]
 [ -4.    64.    54.7   -3.33]]

Bottom right $2 \times 2$ block as a 2D sub-array


In [11]:
print(A[-2:,-2:])


[[   3.64    4.11]
 [-113.2   323.22]]

Sum of the last column


In [12]:
print(A[:,-1].sum())


391.1

Transpose of A


In [13]:
print(A.transpose())


[[   4.     -4.      2.43    1.2 ]
 [   7.     64.     23.2     2.5 ]
 [  -2.43   54.7     3.64 -113.2 ]
 [  67.1    -3.33    4.11  323.22]]

Exercise 07.4 (optional extension)

In a previous exercise you implemented the bisection algorithm to find approximate roots of a mathematical function. Use the SciPy bisection function optimize.bisect (http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.optimize.bisect.html) to find roots of the mathematical function that was used in the previous exercise. Compare the results computed by SciPy and your program from the earlier exercise, and compare the computational time (using %time).


In [3]:
def f(x):
    return x**3 - 6*x**2 + 4*x + 12
    #return x**2 + x - 20 # Roots = -5, 4

In [6]:
def compute_root(f, x0, x1, tol, max_it):
    """Computes the root of f between x0 and x1 using bisection,
        stops if the value of f at the root is under tol or if max_it is reached
        and returns the root, the value of f at the root and the number of iterations"""
    for i in range(max_it):
    
        # Compute x_mid
        x_mid = (x0 + x1) / 2

        # Compute f for the three values
        f_0, f_1, f_mid = f(x0), f(x1), f(x_mid)

        # Check the value of f_0*f_mid to determine how to update the endpoints
        if f_0*f_mid < 0:
            x1 = x_mid
        else:
            x0 = x_mid
        
        # Check if f is under tol
        if abs(f_mid) < tol:
            return x_mid, f_mid, i+1

    # Return the approximate root in case max_it is reached
    return x_mid, f_mid, i+1

# Test for the function f
%time x, f_x, num_it = compute_root(f, x0=3, x1=6, tol=1.0e-6, max_it=1000)

print('Approximate root:', x)
print('Value of f:', f_x)
print('Number of iterations:', num_it)


Wall time: 500 µs
Approximate root: 4.534070134162903
Value of f: -7.047073751209609e-07
Number of iterations: 23

Below we compute the same root using scipy function bisect: the wall time for both functions is very low but the scipy function is considerably better:


In [7]:
from scipy.optimize import bisect as bisect

# Compute the root of f using scipy function
%time x0 = bisect(f, a=3, b=6)

print('Approximate root:', x0)
print('Value of f:', f(x0))


Wall time: 0 ns
Approximate root: 4.534070196722951
Value of f: 2.4584778657299466e-12